package com.example.medicinebox.network

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.IOException
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.SelectionKey
import java.nio.channels.Selector
import java.nio.channels.SocketChannel
import java.util.concurrent.ConcurrentLinkedQueue


class MbClientService() : MbApiService {
    private val connectionLock: Object = Object()
    override var isConnectedSuccess: Boolean = false
    private val socketChannel: SocketChannel? = SocketChannel.open()
    private var sendKey: SelectionKey? = null
    private val address: String = "47.109.94.67"
    private var socketJob: Job? = null
    private var messageHandler: suspend (String) -> Unit = {}
    private val writeQueue = ConcurrentLinkedQueue<ByteBuffer>()

    override fun setMessageHandler(handler: suspend (String) -> Unit) {
        messageHandler = handler
    }

    private fun startService() {
        socketJob = CoroutineScope(Dispatchers.IO).launch {
            try {
                socketChannel?.configureBlocking(false)
                socketChannel?.connect(InetSocketAddress(address, 2933))
                val selector = Selector.open()
                synchronized(this) {
                    sendKey = socketChannel?.register(selector, SelectionKey.OP_CONNECT)
                }
                while (true) {
                    selector.select()
                    val keys = selector.selectedKeys().iterator()
                    while (keys.hasNext()) {
                        val key = keys.next()
                        keys.remove()
                        if (key.isReadable) {
                            handleRead(key)
                        } else if (key.isWritable) {
                            handleWrite(key)
                        } else if (key.isConnectable) {
                            handleConnect(key)
                        }
                    }
                }
            } catch (_: IOException) {
                isConnectedSuccess = false
            }
        }
    }

    init {
        startService()
    }

    override suspend fun sendMessages(message: String) {
        CoroutineScope(Dispatchers.IO).launch {
            synchronized(connectionLock) {
                while (!isConnectedSuccess) {
                    try {
                        connectionLock.wait()
                    } catch (_: InterruptedException) {
                        return@launch
                    }
                }
            }

            val buffer = ByteBuffer.wrap(message.toByteArray())
            synchronized(this) {
                writeQueue.add(buffer)
                if (sendKey != null && sendKey!!.isValid) {
                    sendKey!!.interestOps(sendKey!!.interestOps() or SelectionKey.OP_WRITE)
                    sendKey!!.selector().wakeup()
                }
            }
        }
    }

    override fun restartService() {
        socketJob?.cancel()
        startService()
    }

    @Throws(IOException::class)
    private suspend fun handleRead(key: SelectionKey) {
        val client = key.channel() as SocketChannel
        val buffer = ByteBuffer.allocate(1024)
        val bytesRead = client.read(buffer)
        if (bytesRead == -1) {
            client.close()
            key.cancel()
            return
        }
        buffer.flip()
        val data = ByteArray(buffer.remaining())
        buffer.get(data)
        messageHandler(String(data))
    }

    @Throws(IOException::class)
    private fun handleWrite(key: SelectionKey) {
        val client = key.channel() as SocketChannel
        val buffer: ByteBuffer?
        synchronized(this) {
            buffer = writeQueue.peek()
        }
        if (buffer == null) {
            key.interestOps(SelectionKey.OP_READ)
            return
        }
        client.write(buffer)
        if (!buffer.hasRemaining()) {
            synchronized(this) {
                writeQueue.poll()
                if (writeQueue.isEmpty()) {
                    key.interestOps(SelectionKey.OP_READ)
                }
            }
        }
    }

    @Throws(IOException::class)
    private fun handleConnect(key: SelectionKey) {
        val client = key.channel() as SocketChannel
        if (client.finishConnect()) {
            key.interestOps(SelectionKey.OP_READ)
            synchronized(connectionLock) {
                isConnectedSuccess = true
                connectionLock.notifyAll()
            }
        } else {
            key.cancel()
            client.close()
        }
    }
}
